Memory leaks in JavaScript are caused by unintentionally retaining references to objects that are no longer needed, preventing the garbage collector from reclaiming their memory and leading to gradual memory exhaustion.
JavaScript's garbage collector automatically frees memory that is no longer reachable from the root. A memory leak occurs when the application holds onto references that keep objects reachable long after they are needed, causing memory usage to grow continuously. These leaks often result from subtle programming patterns that create lingering references, especially in long-running applications like single-page apps or Node.js servers where accumulated memory can eventually cause crashes or severe performance degradation.
Undeclared variables: Assigning a value to an undeclared variable automatically creates a property on the global object (window in browsers, global in Node.js). This reference persists for the entire application lifetime .
'this' in global scope: In non-strict mode, using this in a function that is called without an explicit receiver also references the global object .
Unintentional globals: Forgetting var, let, or const is the most common source. Even primitives become problematic if they reference larger structures .
Solution: Use strict mode ('use strict';), linting tools (ESLint), and module bundlers that prevent accidental globals .
DOM event listeners: Adding event listeners to DOM elements without removing them keeps both the listener function and the DOM element alive, even after the element is removed from the page .
Custom event emitters: In Node.js or browser custom events, listeners attached to EventEmitters must be removed when no longer needed .
Observer patterns: MutationObserver, ResizeObserver, and similar APIs retain references to their callbacks and observed targets until disconnected .
Solution: Always remove listeners with removeEventListener, off, or disconnect() when components unmount or elements are removed. Use weak references where available (WeakMap for metadata) .
Hidden references: Storing DOM elements in JavaScript variables or data structures while they are removed from the page keeps them in memory .
Closures capturing DOM: Functions that close over DOM nodes keep those nodes alive as long as the function exists .
Caches of DOM elements: Caching query results or storing elements in maps/arrays without cleanup .
Solution: Nullify references when done, use WeakMap for DOM metadata, and be careful what closures capture .
setInterval without clear: Functions scheduled with setInterval run forever and keep their entire lexical environment alive unless cleared .
setTimeout chains: Recursive setTimeout patterns can accumulate if not properly managed .
Large captured scopes: Closures that capture large objects keep those objects alive even if the closure only needs a small part .
Solution: Always clear intervals/timeouts with clearInterval/clearTimeout. Be mindful of what closures capture—use only needed variables or copy primitives .
Unbounded caches: Storing results in Maps or objects without size limits or expiration causes unbounded growth .
Memoization without limits: Memoizing functions without clearing old entries .
Session storage: Storing increasing amounts of data in sessionStorage/localStorage without cleanup .
Solution: Implement LRU caches, set size limits, use WeakMap for object-keyed caches, or add TTL (time-to-live) for entries .
Legacy IE issues: Older JavaScript engines had trouble with circular references, especially between DOM and JavaScript objects .
Modern GC handles cycles: Current engines like V8 can collect cycles if no external references exist .
Still problematic with listeners: Circular references involving event listeners can still leak if listeners aren't removed .
Solution: Modern engines are safe, but break cycles explicitly when unsure, especially in complex object graphs .
Historical data: Storing ever-growing arrays of logs, metrics, or history in global state .
WebSocket connections: Accumulating connection objects without cleanup .
Streams and buffers: Not properly closing streams or draining buffers .
Solution: Implement pruning strategies, use bounded data structures, and ensure proper cleanup in long-running processes .
Detecting these leaks requires memory profiling tools like Chrome DevTools, Node.js heap snapshots, or automated testing with memlab. The common theme across all causes is the unintentional extension of object lifetimes beyond their useful scope. By understanding these patterns, developers can write more memory-efficient applications and use appropriate data structures (WeakMap, WeakSet) and cleanup patterns (disposables, lifecycle hooks) to ensure memory is reclaimed when no longer needed.